import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.widget.Scroller;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;

/**
 * description:
 *
 * 可左滑弹出选项菜单的 RV
 *
 * Scroll 在 Move 事件中生效
 * Fling  在 Up   事件中生效
 *
 * 实现的功能：
 * 1. 此 RV 中，上下和左右滑动，同时只生效一个
 * 2. 实现距离不足时，滚动不生效；距离足了，直接 全部显示/全部隐藏
 * 3. 如果正在显示菜单栏，点击事件被拦截
 * 4. 拦截点击事件后，如果点击的是非当前 item，则隐藏当前 item。并不做其他响应
 * 5. 拦截点击事件后，如果点击的是当前 item，则隐藏当前 item。并不做其他响应
 * 6. 拦截点击事件后，如果点击的是当前 item，并且有滚动。则当做 Fling 处理
 * 7. item 的点击事件生效
 * 8. 菜单栏的点击事件生效，并且在点击后自动隐藏
 *
 * @author ZhangWenChen
 * date：    2021/4/21 16:38
 * version   1.0
 * modify by
 */
public class SwipeMenuRecyclerView extends RecyclerView {
    private static final String TAG = "SwipeMenuRecyclerView";
    /**
     * 滑动事件的阙值
     * */
    private int scrollSlop;
    /**
     * 拦截模式下，是否有滚动过
     * */
    private boolean isScrollInIntercept;
    /**
     * 滚动相关
     * */
    private Scroller mScroller;
    /**
     * Down 事件的坐标点，用于 Fling 动作
     * */
    private float rawDownX, rawDownY;
    /**
     * Move 事件(上次事件)的坐标点，用于 Scroll 动作
     */
    private float lastX;
    /**
     * 记录的主手指
     * */
    private int pointerId;
    /**
     * 一次事件流中，是水平滑动，还是竖直滑动的标识
     * */
    private boolean isHorScroll, isVerScroll;
    /**
     * Down 事件按下时的 ViewHolder，lastHolder 为手指按下时，正在显示菜单栏的 ViewHolder
     * 拦截模式下，是对 lastHolder 进行操作，非拦截模式下，对 curHolder 进行操作
     * */
    private ViewHolder curHolder, lastHolder;
    /**
     * 菜单是否显示，将 RV 分成两种模式：拦截模式和非拦截模式
     * 菜单栏显示时，二次点击的不是当前显示菜单的 item，就是拦截模式，否则就是非拦截模式
     * */
    private boolean isInterceptMode;

    public SwipeMenuRecyclerView(@NonNull Context context) {
        this(context, null);
    }

    public SwipeMenuRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public SwipeMenuRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init();
    }
    private void init() {
        ViewConfiguration configuration = ViewConfiguration.get(getContext());
        scrollSlop = configuration.getScaledTouchSlop();
        mScroller = new Scroller(getContext());
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        curHolder = null;
        lastHolder = null;
    }

    /**
     * 当需要滚动时，拦截事件
     * */
    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        // 拦截模式下，事件全部拦截
        int pointerIndex;
        float x, y;
        switch (event.getActionMasked()) {
            case MotionEvent.ACTION_DOWN:
                if(!mScroller.isFinished()) {
                    mScroller.forceFinished(true);
                }
                // 避免多指事件干扰
                pointerIndex = event.getActionIndex();
                x = event.getX(pointerIndex);
                y = event.getY(pointerIndex);

                ViewHolder holder = findViewHolder(event);
                // 显示菜单栏，就拦截
                lastHolder = curHolder;
                if(isMenuShowing(lastHolder)) {
                    // 菜单栏显示时，如果二次点击的不是当前显示菜单栏的 item，则需要拦截事件，隐藏菜单栏
                    if(holder != null && lastHolder != null
                        && holder.getAdapterPosition() != lastHolder.getAdapterPosition()) {
                        hideMenu(lastHolder);
                    }
                    // ----------------------------拦截模式----------------------------
                    isInterceptMode = true;
                } else {
                    // ----------------------------非拦截模式----------------------------
                    isInterceptMode = false;
                }
                curHolder = holder;
                // 二次点击时，点击的不是显示菜单的 item，则拦截，隐藏 item
                rawDownX = lastX = x;
                rawDownY = y;
                // Down 事件时，必定有至少一根手指，这个手指的 index 至少为 0
                pointerId = event.getPointerId(0);
                // 避免自动 Scroll 的影响
                isHorScroll = isVerScroll = false;
                isScrollInIntercept = false;
                if(isInterceptMode) {
                    return true;
                }
                return super.onInterceptTouchEvent(event);
            case MotionEvent.ACTION_MOVE:
                // 需要水平或者竖直滚动时，走到自身 View 的 onTouchEvent 方法
                // Move 拦截后，后续的 Up、Cancel 都会交给此 View 处理
                if(isVerScroll || isHorScroll) {
                    return true;
                }
                pointerIndex = event.findPointerIndex(pointerId);
                x = event.getX(pointerIndex);
                y = event.getY(pointerIndex);
                // 优先检测竖直方向的滑动
                if(!isVerScroll && Math.abs(x - rawDownX) < Math.abs(y - rawDownY) && Math.abs(y - rawDownY) > scrollSlop) {
                    isVerScroll = true;
                    return true;
                }
                if(!isHorScroll && Math.abs(x - rawDownX) > Math.abs(y - rawDownY) && Math.abs(x - rawDownX) > scrollSlop) {
                    isHorScroll = true;
                    return true;
                }
                break;
        }
        return super.onInterceptTouchEvent(event);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int pointerIndex;
        switch (event.getActionMasked()) {
            case MotionEvent.ACTION_MOVE:
                // ----------------------------拦截模式----------------------------
                if(isInterceptMode) {
                    if(lastHolder == null || curHolder == null) {
                        return true;
                    }
                    if(lastHolder.getAdapterPosition() != curHolder.getAdapterPosition()) {
                        return true;
                    } else {
                        pointerIndex = event.findPointerIndex(pointerId);
                        // 大于阙值，才进行滚动
                        if(Math.abs(rawDownX - event.getX(pointerIndex)) > scrollSlop) {
                            isScrollInIntercept = true;
                            return dealScroll(lastHolder, event);
                        }
                    }
                }
                // ----------------------------非拦截模式----------------------------
                // 竖直滚动，不额外实现逻辑
                if(isVerScroll) {
                    return super.onTouchEvent(event);
                }
                // 水平滚动
                return dealScroll(curHolder, event);
            case MotionEvent.ACTION_POINTER_UP:
                // 此段代码是 Google 官方写法
                // 抬起的手指的索引
                pointerIndex = event.getActionIndex();
                int tempPointerId = event.getPointerId(pointerIndex);
                // 如果抬起的手指是当前追踪的手指，则换下个手指追踪
                if(tempPointerId == pointerId) {
                    int newPointerIndex = pointerIndex == 0 ? 1 : 0;
                    lastX = event.getX(newPointerIndex);
                    pointerId = event.getPointerId(newPointerIndex);
                }
                return super.onInterceptTouchEvent(event);
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                // ----------------------------拦截模式----------------------------
                if(isInterceptMode) {
                    if(isMenuShowing(lastHolder)) {
                        if(isScrollInIntercept) {
                            dealFling(lastHolder, event);
                        } else {
                            hideMenu(lastHolder);
                        }
                    }
                    return true;
                }
                // ----------------------------非拦截模式----------------------------
                if(isVerScroll) {
                    return super.onTouchEvent(event);
                } else {
                    dealFling(curHolder, event);
                    return true;
                }
        }
        return super.onTouchEvent(event);
    }

    /**
     * Scroll 动作是在 Move 事件中触发的
     *
     * @return true 表示需要滚动，false 不需要滚动
     * */
    private boolean dealScroll(ViewHolder holder, MotionEvent event) {
        if (holder == null) {
            return false;
        }
        int maxScrollDistance = calculateMaxScrollDistance(holder);
        if(maxScrollDistance <= 0) {
            return false;
        }
        int pointerIndex = event.findPointerIndex(pointerId);
        float x = event.getX(pointerIndex);
        int offsetX = (int) (lastX - x);
        // 判断条件范围
        // 手指从右向左移动，布局也从右向左移动，左移到达最大值
        if(offsetX > 0 && holder.itemView.getScrollX() + offsetX > maxScrollDistance) {
            offsetX = maxScrollDistance - holder.itemView.getScrollX();
        }
        // 手指从左向右移动，布局也从左向右移动，右移到达最大值
        if(offsetX < 0 && holder.itemView.getScrollX() + offsetX < 0) {
            offsetX = -holder.itemView.getScrollX();
        }

        // 竖直方向不移动
        curHolder.itemView.scrollBy(offsetX, 0);
        invalidate();

        lastX = x;
        return true;
    }

    /**
     * 是否判定为水平方向的 Fling，如果是，则执行 Fling 动作
     *
     * Fling 动作是在 Up 事件中触发的
     * */
    private boolean dealFling(ViewHolder holder, MotionEvent event) {
        float nowX = event.getX();
        float offsetX = rawDownX - nowX;

        if(holder == null || holder.itemView.getScrollX() <= 0) {
            return false;
        }
        int maxScrollLength = calculateMaxScrollDistance(holder);
        // 没有可滚动的距离
        if(maxScrollLength < 0) {
            return false;
        }
        int dx;
        // 菜单栏显示
        // 拖动距离不足 1/3 时动作不生效
        if(Math.abs(offsetX) < (float) maxScrollLength / 3) {
            if(offsetX > 0) {
                dx = -holder.itemView.getScrollX();
            } else {
                // 回弹不隐藏
                dx = maxScrollLength - holder.itemView.getScrollX();
            }
        } else {
            if(offsetX > 0) {
                dx = maxScrollLength - holder.itemView.getScrollX();
            } else {
                dx = -holder.itemView.getScrollX();
            }
        }

        // 滑动到目标位置
        mScroller.startScroll(holder.itemView.getScrollX(), 0, dx, 0);
        invalidate();

        return true;
    }

    @Override
    public void computeScroll() {
        if(curHolder == null) {
            return;
        }
        if(mScroller.computeScrollOffset()) {
            // 拦截模式下，需要的是上个显示菜单的 item 滚动，而不是当前被点的 item 滚动
            if(isInterceptMode) {
                lastHolder.itemView.scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            } else {
                curHolder.itemView.scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            }
            invalidate();
        }
    }

    /**
     * -1 代表不可滑动
     * */
    private int calculateMaxScrollDistance(ViewHolder viewHolder) {
        if(viewHolder == null) {
            return -1;
        }
        ViewGroup rootViewGroup = viewHolder.itemView.findViewById(R.id.ll_item_menu);
        if(rootViewGroup == null || rootViewGroup.getChildCount() < 0) {
            return -1;
        }
        int result = 0;
        View view;
        // 根布局是水平方向的 LL，直接从左向右加 item 的宽度
        for(int i = 0; i < rootViewGroup.getChildCount(); i++) {
            view = rootViewGroup.getChildAt(i);
            if(view == null || view.getVisibility() == View.GONE) {
                continue;
            }
            result += view.getMeasuredWidth();
        }
        return result;
    }

    private ViewHolder findViewHolder(MotionEvent event) {
        View view = findChildViewUnder(event.getX(), event.getY());
        if(view == null) {
            return null;
        }
        return findContainingViewHolder(view);
    }
    /**
     * 菜单栏是否在显示的标识
     *
     * @return true 表示菜单在显示，false 表示菜单未显示
     * */
    private boolean isMenuShowing(ViewHolder holder) {
        if(holder == null) {
            return false;
        }
        return holder.itemView.getScrollX() > 0;
    }
    /**
     * 隐藏菜单栏
     * */
    private boolean hideMenu(ViewHolder holder) {
        if(holder == null) {
            return false;
        }
        mScroller.startScroll(holder.itemView.getScrollX(), 0, -holder.itemView.getScrollX(), 0);
        invalidate();
        return true;
    }
}